package evs.security;

import java.io.FileNotFoundException;
import java.io.InputStream;
import java.security.Key;
import java.security.KeyPair;
import java.security.KeyStore;
import java.security.KeyStoreException;
import java.security.NoSuchAlgorithmException;
import java.security.PrivateKey;
import java.security.PublicKey;
import java.security.UnrecoverableKeyException;
import java.security.cert.Certificate;
import java.util.ArrayList;
import java.util.List;

import javax.crypto.BadPaddingException;
import javax.crypto.Cipher;
import javax.crypto.IllegalBlockSizeException;
import javax.servlet.ServletRequest;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.xpath.XPath;
import javax.xml.xpath.XPathConstants;
import javax.xml.xpath.XPathExpression;
import javax.xml.xpath.XPathFactory;

import org.apache.commons.codec.binary.Hex;
import org.apache.commons.lang.NotImplementedException;
import org.apache.log4j.Logger;
import org.w3c.dom.Document;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;

import evs.security.util.LocalAddress;

public class SecurityManager implements ISecurityManager {

	private static final Logger securityLog = Logger
			.getLogger(SecurityManager.class);

	private String key_store;
	// private static final String C_KS_PASSWD = "123456";
	private static final String C_KS_TYPE = "JKS";

	private List<LocalAddress> localAddresses;

	private KeyStore keystore;
	private Cipher cipher;

	private String serviceAlias;
	private char[] c_ks_passwd;

	public SecurityManager(String serviceAlias, String password) {
		this.serviceAlias = serviceAlias.toLowerCase();
		this.c_ks_passwd = password.toCharArray();
	}

	public String encrypt(String plaintext, String keyAlias) {
		if(keystore != null){
			String ciphertext;
	
			try {
				Certificate cert = keystore.getCertificate(keyAlias.toLowerCase());
	
				this.cipher.init(Cipher.ENCRYPT_MODE, cert);
				plaintext = plaintext.trim();
				byte[] bytes = plaintext.getBytes("UTF-8");
	
				byte[] encrypted = blockCipher(bytes, Cipher.ENCRYPT_MODE);
	
				char[] encryptedTranspherable = Hex.encodeHex(encrypted);
				ciphertext = new String(encryptedTranspherable);
			} catch (Exception e) {
				securityLog.error(e);
				ciphertext = "";
			}
			return ciphertext;
		} else{
			return plaintext;
		}
	}

	private KeyPair getPrivateKey() {
		try {
			// Get private key
			Key key = keystore.getKey(serviceAlias, c_ks_passwd);
			if (key instanceof PrivateKey) {
				// Get certificate of public key
				java.security.cert.Certificate cert = keystore
						.getCertificate(serviceAlias);

				// Get public key
				PublicKey publicKey = cert.getPublicKey();

				// Return a key pair
				return new KeyPair(publicKey, (PrivateKey) key);
			}
		} catch (UnrecoverableKeyException e) {
		} catch (NoSuchAlgorithmException e) {
		} catch (KeyStoreException e) {
		}
		return null;
	}

	@Override
	public String decryptWithPrivateKey(String encrypted) {
		if(keystore != null){
			String decrypted;
	
			try {
				this.cipher.init(Cipher.DECRYPT_MODE, getPrivateKey().getPrivate());
				byte[] bts = Hex.decodeHex(encrypted.toCharArray());
	
				byte[] decryptedText = blockCipher(bts, Cipher.DECRYPT_MODE);
	
				decrypted = new String(decryptedText, "UTF-8");
			} catch (Exception e) {
				securityLog.error(e);
				decrypted = "";
			}
			return decrypted.trim();
		} else{
			return encrypted;
		}
	}

	public String decrypt(String encrypted, String keyAlias) {
		if(keystore != null){
			String decrypted;
	
			try {
				Certificate cert = keystore.getCertificate(keyAlias.toLowerCase());
	
				this.cipher.init(Cipher.DECRYPT_MODE, cert);
	
				byte[] bts = Hex.decodeHex(encrypted.toCharArray());
	
				byte[] decryptedText = blockCipher(bts, Cipher.DECRYPT_MODE);
	
				decrypted = new String(decryptedText, "UTF-8");
			} catch (Exception e) {
				securityLog.error(e);
				decrypted = "";
			}
			return decrypted.trim();
		} else{
			return encrypted;
		}
	}

	@Override
	public String encryptWithPrivateKey(String plaintext) {
		if(keystore != null){
			String ciphertext;
			try {
	
				this.cipher.init(Cipher.ENCRYPT_MODE, getPrivateKey().getPrivate());
				plaintext = plaintext.trim();
				byte[] bytes = plaintext.getBytes("UTF-8");
	
				byte[] encrypted = blockCipher(bytes, Cipher.ENCRYPT_MODE);
	
				char[] encryptedTranspherable = Hex.encodeHex(encrypted);
				ciphertext = new String(encryptedTranspherable);
			} catch (Exception e) {
				securityLog.error(e);
				ciphertext = "";
			}
			return ciphertext;
		} else{
			return plaintext;
		}
	}

	@Override
	public void initialize(String configFileName) throws Exception {

		InputStream stream = this.getClass().getClassLoader()
				.getResourceAsStream(configFileName);

		if (stream == null) {
			throw new FileNotFoundException("Config File " + configFileName
					+ " for Securitymanager was not found!");
		}

		try {
			DocumentBuilderFactory domFactory = DocumentBuilderFactory
					.newInstance();
			domFactory.setNamespaceAware(true);
			DocumentBuilder builder = domFactory.newDocumentBuilder();
			Document doc = builder.parse(stream);

			XPathFactory factory = XPathFactory.newInstance();

			// read store
			XPath xpath = factory.newXPath();
			XPathExpression expr = xpath
					.compile("//security-config/store/text()");

			key_store = (String) expr.evaluate(doc, XPathConstants.STRING);

			// readAddresses
			xpath = factory.newXPath();
			expr = xpath.compile("//security-config/local/address");

			Object result = expr.evaluate(doc, XPathConstants.NODESET);
			NodeList nodes = (NodeList) result;
			localAddresses = new ArrayList<LocalAddress>();
			for (int i = 0; i < nodes.getLength(); i++) {
				Node node = nodes.item(i);
				String address = node.getAttributes().getNamedItem("ip")
						.getNodeValue();
				String netmask = node.getAttributes().getNamedItem("netmask")
						.getNodeValue();
				localAddresses.add(new LocalAddress(address, netmask));
			}

		} catch (Exception ex) {
			throw new Exception("Config File " + configFileName
					+ " has the wrong format!");
		}

		try {
			if(!key_store.trim().equals("")){
				this.keystore = KeyStore.getInstance(C_KS_TYPE);
				this.keystore.load(Thread.currentThread().getContextClassLoader()
						.getResourceAsStream(key_store), c_ks_passwd);
				this.cipher = Cipher.getInstance("RSA/ECB/PKCS1Padding");
			} else{
				this.keystore = null;
			}
		} catch (Exception ex) {
			throw new Exception("Wrong Password!");
		}
	}

	@Override
	public boolean isLocalRequest(ServletRequest request) {
		String ip = request.getRemoteAddr();

		for (LocalAddress a : localAddresses) {
			if (a.isIn(ip)) {
				return true;
			}
		}
		return false;
	}

	private byte[] blockCipher(byte[] bytes, int mode)
			throws IllegalBlockSizeException, BadPaddingException {
		// string initialize 2 buffers.
		// scrambled will hold intermediate results
		byte[] scrambled = new byte[0];

		// toReturn will hold the total result
		byte[] toReturn = new byte[0];
		// if we encrypt we use 100 byte long blocks. Decryption requires 128
		// byte long blocks (because of RSA)
		int length = (mode == Cipher.ENCRYPT_MODE) ? 100 : 128;

		// another buffer. this one will hold the bytes that have to be modified
		// in this step
		byte[] buffer = new byte[length];

		for (int i = 0; i < bytes.length; i++) {

			// if we filled our buffer array we have our block ready for de- or
			// encryption
			if ((i > 0) && (i % length == 0)) {
				// execute the operation
				scrambled = cipher.doFinal(buffer);
				// add the result to our total result.
				toReturn = append(toReturn, scrambled);
				// here we calculate the length of the next buffer required
				int newlength = length;

				// if newlength would be longer than remaining bytes in the
				// bytes array we shorten it.
				if (i + length > bytes.length) {
					newlength = bytes.length - i;
				}
				// clean the buffer array
				buffer = new byte[newlength];
			}
			// copy byte into our buffer.
			buffer[i % length] = bytes[i];
		}

		// this step is needed if we had a trailing buffer. should only happen
		// when encrypting.
		// example: we encrypt 110 bytes. 100 bytes per run means we "forgot"
		// the last 10 bytes. they are in the buffer array
		scrambled = cipher.doFinal(buffer);

		// final step before we can return the modified data.
		toReturn = append(toReturn, scrambled);

		return toReturn;
	}

	private byte[] append(byte[] prefix, byte[] suffix) {
		byte[] toReturn = new byte[prefix.length + suffix.length];
		for (int i = 0; i < prefix.length; i++) {
			toReturn[i] = prefix[i];
		}
		for (int i = 0; i < suffix.length; i++) {
			toReturn[i + prefix.length] = suffix[i];
		}
		return toReturn;
	}

}
